package NES::M6502;

use strict;
use Data::Dumper;
use Utils qw|test_bit|;

use constant {
    # debug mode
    DEBUG => 0,

    # addressing modes
    ABSOLUTE      => 1,
    IMMEDIATE     => 2,
    ZERO_PAGE     => 3,
    IMPLIED       => 4,
    RELATIVE      => 5,
    ACCUMULATOR   => 6,
    INDIRECT_X    => 7,
    INDIRECT_Y    => 8,
    INDIRECT_Y_NC => 9,
    INDIRECT      => 10,
    ABSOLUTE_X    => 11,
    ABSOLUTE_X_NC => 12,
    ABSOLUTE_Y    => 13,
    ABSOLUTE_Y_NC => 14,
    ZERO_PAGE_X   => 15,
    ZERO_PAGE_Y   => 16,

    # time multiplier
    # TIME_MULTIPLIER => 3, # FIXME: solo para DEBUG
    TIME_MULTIPLIER => 1,
};

sub new {
    my $class = shift;

    my $self = bless {
        opc        => {},
        addr       => {},
        debug      => {},
        cyc        => {},
        read       => undef,
        write      => undef,
        pc         => { h => 0xc0, l => 0x00 }, # FIXME: nestest.nes only!
        sp         => 0xfd,
        reg        => { a => 0, x => 0, y => 0 },
        flag       => { n => 0, v => 0, p => 1, b => 0, d => 0, i => 1, z => 0, c => 0 },
        cycles     => 0,
        @_,
    }, $class;

    $self->{opc}   = $self->create_jump_table();
    $self->{cyc}   = $self->create_cycles_table();
    $self->{addr}  = $self->create_addressing_table();
    $self->{debug} = $self->create_debugger();

    $self->{pc}{h} = $self->read( 0xfffd );
    $self->{pc}{l} = $self->read( 0xfffc );

    return $self;
}

sub join_flags {
    my $self = shift;

    my ( $value, $i ) = ( 0, 7 );
    $value += ( $_ << $i-- ) for @{ $self->{flag} }{ qw|n v p b d i z c| };

    return ( $value & 0x00ff );
}

sub clear_flags {
    my $self = shift;
    my ( $flags ) = @_;

    $self->{flag}{$_} = 0 for @$flags;
}

sub set_flags {
    my $self = shift;
    my ( $flags ) = @_;

    $self->{flag}{$_} = 1 for @$flags;
}

sub create_jump_table {
    my $self = shift;

    return {
        # branch
        0x10 => sub { return $self->opcode_branch( mode => RELATIVE, condition => (! $self->{flag}{n}) ) },
        0x30 => sub { return $self->opcode_branch( mode => RELATIVE, condition =>    $self->{flag}{n}  ) },
        0x50 => sub { return $self->opcode_branch( mode => RELATIVE, condition => (! $self->{flag}{v}) ) },
        0x70 => sub { return $self->opcode_branch( mode => RELATIVE, condition =>    $self->{flag}{v}  ) },
        0x90 => sub { return $self->opcode_branch( mode => RELATIVE, condition => (! $self->{flag}{c}) ) },
        0xb0 => sub { return $self->opcode_branch( mode => RELATIVE, condition =>    $self->{flag}{c}  ) },
        0xd0 => sub { return $self->opcode_branch( mode => RELATIVE, condition => (! $self->{flag}{z}) ) },
        0xf0 => sub { return $self->opcode_branch( mode => RELATIVE, condition =>    $self->{flag}{z}  ) },

        # test bit
        0x24 => sub { return $self->opcode_test_bit( mode => ZERO_PAGE ) },
        0x2c => sub { return $self->opcode_test_bit( mode => ABSOLUTE  ) },
        
        # add with carry
        0x61 => sub { return $self->opcode_add_with_carry( mode => INDIRECT_X  ) },
        0x65 => sub { return $self->opcode_add_with_carry( mode => ZERO_PAGE   ) },
        0x69 => sub { return $self->opcode_add_with_carry( mode => IMMEDIATE   ) },
        0x6d => sub { return $self->opcode_add_with_carry( mode => ABSOLUTE    ) },
        0x71 => sub { return $self->opcode_add_with_carry( mode => INDIRECT_Y  ) },
        0x75 => sub { return $self->opcode_add_with_carry( mode => ZERO_PAGE_X ) },
        0x79 => sub { return $self->opcode_add_with_carry( mode => ABSOLUTE_Y  ) },
        0x7d => sub { return $self->opcode_add_with_carry( mode => ABSOLUTE_X  ) },

        # subtract with carry
        0xe1 => sub { return $self->opcode_subtract_with_carry( mode => INDIRECT_X  ) },
        0xe5 => sub { return $self->opcode_subtract_with_carry( mode => ZERO_PAGE   ) },
        0xe9 => sub { return $self->opcode_subtract_with_carry( mode => IMMEDIATE   ) },
        0xed => sub { return $self->opcode_subtract_with_carry( mode => ABSOLUTE    ) },
        0xf1 => sub { return $self->opcode_subtract_with_carry( mode => INDIRECT_Y  ) },
        0xf5 => sub { return $self->opcode_subtract_with_carry( mode => ZERO_PAGE_X ) },
        0xf9 => sub { return $self->opcode_subtract_with_carry( mode => ABSOLUTE_Y  ) },
        0xfd => sub { return $self->opcode_subtract_with_carry( mode => ABSOLUTE_X  ) },

        # logical and
        0x21 => sub { return $self->opcode_logical_and( mode => INDIRECT_X  ) },
        0x25 => sub { return $self->opcode_logical_and( mode => ZERO_PAGE   ) },
        0x29 => sub { return $self->opcode_logical_and( mode => IMMEDIATE   ) },
        0x2d => sub { return $self->opcode_logical_and( mode => ABSOLUTE    ) },
        0x31 => sub { return $self->opcode_logical_and( mode => INDIRECT_Y  ) },
        0x35 => sub { return $self->opcode_logical_and( mode => ZERO_PAGE_X ) },
        0x39 => sub { return $self->opcode_logical_and( mode => ABSOLUTE_Y  ) },
        0x3d => sub { return $self->opcode_logical_and( mode => ABSOLUTE_X  ) },

        # logical xor
        0x41 => sub { return $self->opcode_logical_xor( mode => INDIRECT_X  ) },
        0x45 => sub { return $self->opcode_logical_xor( mode => ZERO_PAGE   ) },
        0x49 => sub { return $self->opcode_logical_xor( mode => IMMEDIATE   ) },
        0x4d => sub { return $self->opcode_logical_xor( mode => ABSOLUTE    ) },
        0x51 => sub { return $self->opcode_logical_xor( mode => INDIRECT_Y  ) },
        0x55 => sub { return $self->opcode_logical_xor( mode => ZERO_PAGE_X ) },
        0x59 => sub { return $self->opcode_logical_xor( mode => ABSOLUTE_Y  ) },
        0x5d => sub { return $self->opcode_logical_xor( mode => ABSOLUTE_X  ) },

        # logical or
        0x01 => sub { return $self->opcode_logical_or( mode => INDIRECT_X  ) },
        0x05 => sub { return $self->opcode_logical_or( mode => ZERO_PAGE   ) },
        0x09 => sub { return $self->opcode_logical_or( mode => IMMEDIATE   ) },
        0x0d => sub { return $self->opcode_logical_or( mode => ABSOLUTE    ) },
        0x11 => sub { return $self->opcode_logical_or( mode => INDIRECT_Y  ) },
        0x15 => sub { return $self->opcode_logical_or( mode => ZERO_PAGE_X ) },
        0x19 => sub { return $self->opcode_logical_or( mode => ABSOLUTE_Y  ) },
        0x1d => sub { return $self->opcode_logical_or( mode => ABSOLUTE_X  ) },

        # shift right
        0x46 => sub { return $self->opcode_shift_right( mode => ZERO_PAGE     ) },
        0x4a => sub { return $self->opcode_shift_right( mode => ACCUMULATOR   ) },
        0x4e => sub { return $self->opcode_shift_right( mode => ABSOLUTE      ) },
        0x56 => sub { return $self->opcode_shift_right( mode => ZERO_PAGE_X   ) },
        0x5e => sub { return $self->opcode_shift_right( mode => ABSOLUTE_X_NC ) },

        # shift left
        0x06 => sub { return $self->opcode_shift_left( mode => ZERO_PAGE     ) },
        0x0a => sub { return $self->opcode_shift_left( mode => ACCUMULATOR   ) },
        0x0e => sub { return $self->opcode_shift_left( mode => ABSOLUTE      ) },
        0x16 => sub { return $self->opcode_shift_left( mode => ZERO_PAGE_X   ) },
        0x1e => sub { return $self->opcode_shift_left( mode => ABSOLUTE_X_NC ) },

        # rotate right
        0x66 => sub { return $self->opcode_rotate_right( mode => ZERO_PAGE     ) },
        0x6a => sub { return $self->opcode_rotate_right( mode => ACCUMULATOR   ) },
        0x6e => sub { return $self->opcode_rotate_right( mode => ABSOLUTE      ) },
        0x76 => sub { return $self->opcode_rotate_right( mode => ZERO_PAGE_X   ) },
        0x7e => sub { return $self->opcode_rotate_right( mode => ABSOLUTE_X_NC ) },

        # rotate left
        0x26 => sub { return $self->opcode_rotate_left( mode => ZERO_PAGE     ) },
        0x2a => sub { return $self->opcode_rotate_left( mode => ACCUMULATOR   ) },
        0x2e => sub { return $self->opcode_rotate_left( mode => ABSOLUTE      ) },
        0x36 => sub { return $self->opcode_rotate_left( mode => ZERO_PAGE_X   ) },
        0x3e => sub { return $self->opcode_rotate_left( mode => ABSOLUTE_X_NC ) },

        # clear flag
        0x18 => sub { return $self->opcode_clear_flag( mode => IMPLIED, flag => 'c' ) },
        0xb8 => sub { return $self->opcode_clear_flag( mode => IMPLIED, flag => 'v' ) },
        0xd8 => sub { return $self->opcode_clear_flag( mode => IMPLIED, flag => 'd' ) },

        # set flag
        0x38 => sub { return $self->opcode_set_flag( mode => IMPLIED, flag => 'c' ) },
        0x78 => sub { return $self->opcode_set_flag( mode => IMPLIED, flag => 'i' ) },
        0xf8 => sub { return $self->opcode_set_flag( mode => IMPLIED, flag => 'd' ) },

        # load register
        0xa0 => sub { return $self->opcode_load_reg( mode => IMMEDIATE  , reg => 'y' ) },
        0xa1 => sub { return $self->opcode_load_reg( mode => INDIRECT_X , reg => 'a' ) },
        0xa2 => sub { return $self->opcode_load_reg( mode => IMMEDIATE  , reg => 'x' ) },
        0xa4 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE  , reg => 'y' ) },
        0xa5 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE  , reg => 'a' ) },
        0xa6 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE  , reg => 'x' ) },
        0xa9 => sub { return $self->opcode_load_reg( mode => IMMEDIATE  , reg => 'a' ) },
        0xac => sub { return $self->opcode_load_reg( mode => ABSOLUTE   , reg => 'y' ) },
        0xad => sub { return $self->opcode_load_reg( mode => ABSOLUTE   , reg => 'a' ) },
        0xae => sub { return $self->opcode_load_reg( mode => ABSOLUTE   , reg => 'x' ) },
        0xb1 => sub { return $self->opcode_load_reg( mode => INDIRECT_Y , reg => 'a' ) },
        0xb4 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE_X, reg => 'y' ) },
        0xb5 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE_X, reg => 'a' ) },
        0xb6 => sub { return $self->opcode_load_reg( mode => ZERO_PAGE_Y, reg => 'x' ) },
        0xb9 => sub { return $self->opcode_load_reg( mode => ABSOLUTE_Y , reg => 'a' ) },
        0xbc => sub { return $self->opcode_load_reg( mode => ABSOLUTE_X , reg => 'y' ) },
        0xbd => sub { return $self->opcode_load_reg( mode => ABSOLUTE_X , reg => 'a' ) },
        0xbe => sub { return $self->opcode_load_reg( mode => ABSOLUTE_Y , reg => 'x' ) },

        # store register
        0x81 => sub { return $self->opcode_store_reg( mode => INDIRECT_X   , reg => 'a' ) },
        0x84 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE    , reg => 'y' ) },
        0x85 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE    , reg => 'a' ) },
        0x86 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE    , reg => 'x' ) },
        0x8c => sub { return $self->opcode_store_reg( mode => ABSOLUTE     , reg => 'y' ) },
        0x8d => sub { return $self->opcode_store_reg( mode => ABSOLUTE     , reg => 'a' ) },
        0x8e => sub { return $self->opcode_store_reg( mode => ABSOLUTE     , reg => 'x' ) },
        0x91 => sub { return $self->opcode_store_reg( mode => INDIRECT_Y_NC, reg => 'a' ) },
        0x94 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE_X  , reg => 'y' ) },
        0x95 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE_X  , reg => 'a' ) },
        0x96 => sub { return $self->opcode_store_reg( mode => ZERO_PAGE_Y  , reg => 'x' ) },
        0x99 => sub { return $self->opcode_store_reg( mode => ABSOLUTE_Y_NC, reg => 'a' ) },
        0x9d => sub { return $self->opcode_store_reg( mode => ABSOLUTE_X_NC, reg => 'a' ) },

        # compare register
        0xc0 => sub { return $self->opcode_compare_reg( mode => IMMEDIATE  , reg => 'y' ) },
        0xc1 => sub { return $self->opcode_compare_reg( mode => INDIRECT_X , reg => 'a' ) },
        0xc4 => sub { return $self->opcode_compare_reg( mode => ZERO_PAGE  , reg => 'y' ) },
        0xc5 => sub { return $self->opcode_compare_reg( mode => ZERO_PAGE  , reg => 'a' ) },
        0xc9 => sub { return $self->opcode_compare_reg( mode => IMMEDIATE  , reg => 'a' ) },
        0xcc => sub { return $self->opcode_compare_reg( mode => ABSOLUTE   , reg => 'y' ) },
        0xcd => sub { return $self->opcode_compare_reg( mode => ABSOLUTE   , reg => 'a' ) },
        0xd1 => sub { return $self->opcode_compare_reg( mode => INDIRECT_Y , reg => 'a' ) },
        0xd5 => sub { return $self->opcode_compare_reg( mode => ZERO_PAGE_X, reg => 'a' ) },
        0xd9 => sub { return $self->opcode_compare_reg( mode => ABSOLUTE_Y , reg => 'a' ) },
        0xdd => sub { return $self->opcode_compare_reg( mode => ABSOLUTE_X , reg => 'a' ) },
        0xe0 => sub { return $self->opcode_compare_reg( mode => IMMEDIATE  , reg => 'x' ) },
        0xe4 => sub { return $self->opcode_compare_reg( mode => ZERO_PAGE  , reg => 'x' ) },
        0xec => sub { return $self->opcode_compare_reg( mode => ABSOLUTE   , reg => 'x' ) },

        # increment register
        0xc8 => sub { return $self->opcode_increment_reg( mode => IMPLIED, reg => 'y' ) },
        0xe8 => sub { return $self->opcode_increment_reg( mode => IMPLIED, reg => 'x' ) },

        # decrement register
        0x88 => sub { return $self->opcode_decrement_reg( mode => IMPLIED, reg => 'y' ) },
        0xca => sub { return $self->opcode_decrement_reg( mode => IMPLIED, reg => 'x' ) },

        # increment memory
        0xe6 => sub { return $self->opcode_increment_memory( mode => ZERO_PAGE     ) },
        0xee => sub { return $self->opcode_increment_memory( mode => ABSOLUTE      ) },
        0xf6 => sub { return $self->opcode_increment_memory( mode => ZERO_PAGE_X   ) },
        0xfe => sub { return $self->opcode_increment_memory( mode => ABSOLUTE_X_NC ) },

        # decrement memory
        0xc6 => sub { return $self->opcode_decrement_memory( mode => ZERO_PAGE     ) },
        0xce => sub { return $self->opcode_decrement_memory( mode => ABSOLUTE      ) },
        0xd6 => sub { return $self->opcode_decrement_memory( mode => ZERO_PAGE_X   ) },
        0xde => sub { return $self->opcode_decrement_memory( mode => ABSOLUTE_X_NC ) },

        # transfer register
        0x8a => sub { return $self->opcode_transfer_reg( mode => IMPLIED, from => 'x', to => 'a' ) },
        0x98 => sub { return $self->opcode_transfer_reg( mode => IMPLIED, from => 'y', to => 'a' ) },
        0xa8 => sub { return $self->opcode_transfer_reg( mode => IMPLIED, from => 'a', to => 'y' ) },
        0xaa => sub { return $self->opcode_transfer_reg( mode => IMPLIED, from => 'a', to => 'x' ) },

        # transfer stack pointer to register
        0xba => sub { return $self->opcode_transfer_stack_pointer_to_reg( mode => IMPLIED, to => 'x' ) },

        # transfer register to stack pointer
        0x9a => sub { return $self->opcode_transfer_reg_to_stack_pointer( mode => IMPLIED, from => 'x' ) },

        # jump
        0x4c => sub { return $self->opcode_jump( mode => ABSOLUTE ) },
        0x6c => sub { return $self->opcode_jump( mode => INDIRECT ) },

        # jump to subroutine
        0x20 => sub { return $self->opcode_jump_to_subroutine( mode => ABSOLUTE ) },

        # return from subroutine
        0x60 => sub { return $self->opcode_return_from_subroutine( mode => IMPLIED ) },

        # return from interrupt
        0x40 => sub { return $self->opcode_return_from_interrupt( mode => IMPLIED ) },

        # no operation
        0xea => sub { return $self->opcode_no_operation( mode => IMPLIED ) },

        # push status
        0x08 => sub { return $self->opcode_push_status( mode => IMPLIED ) },

        # pull status
        0x28 => sub { return $self->opcode_pull_status( mode => IMPLIED ) },

        # push accumulator
        0x48 => sub { return $self->opcode_push_accumulator( mode => IMPLIED ) },

        # pull accumulator
        0x68 => sub { return $self->opcode_pull_accumulator( mode => IMPLIED ) },
    };
}

sub create_cycles_table {
    my $self = shift;

    return {
        0x01 => 6, 0x05 => 3, 0x06 => 5, 0x08 => 3, 0x09 => 2, 0x0a => 2, 0x0d => 4, 0x0e => 6, 0x10 => 2, 0x11 => 5, 0x15 => 4, 0x16 => 6, 0x18 => 2,
        0x19 => 4, 0x1d => 4, 0x1e => 7, 0x20 => 6, 0x21 => 6, 0x24 => 3, 0x25 => 3, 0x26 => 5, 0x28 => 4, 0x29 => 2, 0x2a => 2, 0x2c => 4, 0x2d => 4,
        0x2e => 6, 0x30 => 2, 0x31 => 5, 0x35 => 4, 0x36 => 6, 0x38 => 2, 0x39 => 4, 0x3d => 4, 0x3e => 7, 0x40 => 6, 0x41 => 6, 0x45 => 3, 0x46 => 5,
        0x48 => 3, 0x49 => 2, 0x4a => 2, 0x4c => 3, 0x4d => 4, 0x4e => 6, 0x50 => 2, 0x51 => 5, 0x55 => 4, 0x56 => 6, 0x59 => 4, 0x5d => 4, 0x5e => 7,
        0x60 => 6, 0x61 => 6, 0x65 => 3, 0x66 => 5, 0x68 => 4, 0x69 => 2, 0x6a => 2, 0x6c => 5, 0x6d => 4, 0x6e => 6, 0x70 => 2, 0x71 => 5, 0x75 => 4,
        0x76 => 6, 0x78 => 2, 0x79 => 4, 0x7d => 4, 0x7e => 7, 0x81 => 6, 0x84 => 3, 0x85 => 3, 0x86 => 3, 0x88 => 2, 0x8a => 2, 0x8c => 4, 0x8d => 4,
        0x8e => 4, 0x90 => 2, 0x91 => 6, 0x94 => 4, 0x95 => 4, 0x96 => 4, 0x98 => 2, 0x99 => 5, 0x9a => 2, 0x9d => 5, 0xa0 => 2, 0xa1 => 6, 0xa2 => 2,
        0xa4 => 3, 0xa5 => 3, 0xa6 => 3, 0xa8 => 2, 0xa9 => 2, 0xaa => 2, 0xac => 4, 0xad => 4, 0xae => 4, 0xb0 => 2, 0xb1 => 5, 0xb4 => 4, 0xb5 => 4,
        0xb6 => 4, 0xb8 => 2, 0xb9 => 4, 0xba => 2, 0xbc => 4, 0xbd => 4, 0xbe => 4, 0xc0 => 2, 0xc1 => 6, 0xc4 => 3, 0xc5 => 3, 0xc6 => 5, 0xc8 => 2,
        0xc9 => 2, 0xca => 2, 0xcc => 4, 0xcd => 4, 0xce => 6, 0xd0 => 2, 0xd1 => 5, 0xd5 => 4, 0xd6 => 6, 0xd8 => 2, 0xd9 => 4, 0xdd => 4, 0xde => 7,
        0xe0 => 2, 0xe1 => 6, 0xe4 => 3, 0xe5 => 3, 0xe6 => 5, 0xe8 => 2, 0xe9 => 2, 0xea => 2, 0xec => 4, 0xed => 4, 0xee => 6, 0xf0 => 2, 0xf1 => 5,
        0xf5 => 4, 0xf6 => 6, 0xf8 => 2, 0xf9 => 4, 0xfd => 4, 0xfe => 7,
    };
}

sub create_addressing_table {
    my $self = shift;

    return {
        1 => sub { # absolute
            return ( ($self->fetch | ($self->fetch << 8)) & 0xffff );
        },
        2 => sub { # immediate
            return $self->pc_increment;
        },
        3 => sub { # zero page
            return $self->fetch,
        },
        4 => sub { # implied
            return $self->pc,
        },
        5 => sub { # relative
            my $before = ( (($self->pc + 1) >> 8) & 0x00ff );

            my $disp = unpack( 'c', (pack 'c', $self->fetch) );

            my $addr = ( ($self->pc + $disp) & 0xffff );
            $self->{pc}{h} = ( ($addr >> 8) & 0x00ff );
            $self->{pc}{l} = ( $addr & 0x00ff );

            $self->{cycles} += ( ($before != $self->{pc}{h} ? 2 : 1) * TIME_MULTIPLIER );

            return $self->pc;
        },
        6 => sub { # accumulator
            return $self->{reg}{a},
        },
        7 => sub { # indirect x
            my $l = ( ($self->read($self->pc) + $self->{reg}{x}) & 0x00ff );
            $l = $self->read( $l );

            my $h = ( ($self->fetch + $self->{reg}{x} + 1) & 0x00ff );
            $h = $self->read( $h );

            return ( (($h << 8) | $l) & 0xffff );
        },
        8 => sub { # indirect y
            my $l = $self->read( $self->pc );
            $l = $self->read( $l );

            my $h = ( ($self->fetch + 1) & 0x00ff );
            $h = $self->read( $h );

            my $before = $h;

            my $address = ( ((($h << 8) | $l) + $self->{reg}{y}) & 0xffff );

            $self->{cycles} = ( $self->{cycles} + 1 * TIME_MULTIPLIER ) if( $before != (($address >> 8) & 0x00ff) );

            return $address;
        },
        9 => sub { # indirect y (no page-cross)
            my $l = $self->read( $self->pc );
            $l = $self->read( $l );

            my $h = ( ($self->fetch + 1) & 0x00ff );
            $h = $self->read( $h );

            return ( ((($h << 8) | $l) + $self->{reg}{y}) & 0xffff );
        },
        10 => sub { # indirect
            my $l1       = $self->fetch;
            my $h1       = $self->read( $self->pc );
            my $address1 = ( (($h1 << 8) | ($l1++)) & 0xffff );

            my $l2    = $self->read( $address1 );
            $address1 = ( (($h1 << 8) | ($l1 & 0x00ff)) & 0xffff );
            my $h2    = $self->read( $address1 );

            return ( (($h2 << 8) | $l2) & 0xffff );
        },
        11 => sub { # absolute x
            my $l = $self->fetch;
            my $h = $self->fetch;

            my $before = $h;

            my $address = ( ((($h << 8) | $l) + $self->{reg}{x}) & 0xffff );

            $self->{cycles} = ( $self->{cycles} + 1 * TIME_MULTIPLIER ) if( $before != (($address >> 8) & 0x00ff) );

            return $address;
        },
        12 => sub { # absolute x (no page-cross)
            my $l = $self->fetch;
            my $h = $self->fetch;

            return ( ((($h << 8) | $l) + $self->{reg}{x}) & 0xffff );
        },
        13 => sub { # absolute y
            my $l = $self->fetch;
            my $h = $self->fetch;

            my $before = $h;

            my $address = ( ((($h << 8) | $l) + $self->{reg}{y}) & 0xffff );

            $self->{cycles} = ( $self->{cycles} + 1 * TIME_MULTIPLIER ) if( $before != (($address >> 8) & 0x00ff) );

            return $address;
        },
        14 => sub { # absolute y (no page-cross)
            my $l = $self->fetch;
            my $h = $self->fetch;

            return ( ((($h << 8) | $l) + $self->{reg}{y}) & 0xffff );
        },
        15 => sub { # zero page x
            return ( ($self->fetch + $self->{reg}{x}) & 0x00ff );
        },
        16 => sub { # zero page y
            return ( ($self->fetch + $self->{reg}{y}) & 0x00ff );
        },
    };
}

sub create_debugger {
    my $self = shift;

    return {
        status => sub { return ( sprintf "A:%02X X:%02X Y:%02X P:%02X SP:%02X CYC:%3i", $self->{reg}{a}, $self->{reg}{x}, $self->{reg}{y}, $self->join_flags, $self->{sp}, $self->{cycles} ) },
        0x01   => sub { return ( sprintf "%02X     ORA (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0x05   => sub { return ( sprintf "%02X     ORA \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x06   => sub { return ( sprintf "%02X     ASL \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x08   => sub { return ( sprintf "       PHP                             " ) },
        0x09   => sub { return ( sprintf "%02X     ORA #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0x0a   => sub { return ( sprintf "       ASL A                           " ) },
        0x0d   => sub { return ( sprintf "%02X %02X  ORA \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x0e   => sub { return ( sprintf "%02X %02X  ASL \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x10   => sub { return ( sprintf "%02X     BPL \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0x11   => sub { return ( sprintf "%02X     ORA (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0x15   => sub { return ( sprintf "%02X     ORA \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x16   => sub { return ( sprintf "%02X     ASL \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x18   => sub { return ( sprintf "       CLC                             " ) },
        0x19   => sub { return ( sprintf "%02X %02X  ORA \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0x1d   => sub { return ( sprintf "%02X %02X  ORA \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x1e   => sub { return ( sprintf "%02X %02X  ASL \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x20   => sub { return ( sprintf "%02X %02X  JSR \$%02X%02X                       ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0) ) },
        0x21   => sub { return ( sprintf "%02X     AND (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0x24   => sub { return ( sprintf "%02X     BIT \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x25   => sub { return ( sprintf "%02X     AND \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x26   => sub { return ( sprintf "%02X     ROL \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x28   => sub { return ( sprintf "       PLP                             " ) },
        0x29   => sub { return ( sprintf "%02X     AND #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0x2a   => sub { return ( sprintf "       ROL A                           " ) },
        0x2c   => sub { return ( sprintf "%02X %02X  BIT \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x2d   => sub { return ( sprintf "%02X %02X  AND \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x2e   => sub { return ( sprintf "%02X %02X  ROL \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x30   => sub { return ( sprintf "%02X     BMI \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0x31   => sub { return ( sprintf "%02X     AND (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0x35   => sub { return ( sprintf "%02X     AND \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x36   => sub { return ( sprintf "%02X     ROL \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x38   => sub { return ( sprintf "       SEC                             " ) },
        0x39   => sub { return ( sprintf "%02X %02X  AND \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0x3d   => sub { return ( sprintf "%02X %02X  AND \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x3e   => sub { return ( sprintf "%02X %02X  ROL \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x40   => sub { return ( sprintf "       RTI                             " ) },
        0x41   => sub { return ( sprintf "%02X     EOR (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0x45   => sub { return ( sprintf "%02X     EOR \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x46   => sub { return ( sprintf "%02X     LSR \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x48   => sub { return ( sprintf "       PHA                             " ) },
        0x49   => sub { return ( sprintf "%02X     EOR #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0x4a   => sub { return ( sprintf "       LSR A                           " ) },
        0x4c   => sub { return ( sprintf "%02X %02X  JMP \$%02X%02X                       ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0) ) },
        0x4e   => sub { return ( sprintf "%02X %02X  LSR \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x4d   => sub { return ( sprintf "%02X %02X  EOR \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x50   => sub { return ( sprintf "%02X     BVC \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0x51   => sub { return ( sprintf "%02X     EOR (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0x55   => sub { return ( sprintf "%02X     EOR \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x56   => sub { return ( sprintf "%02X     LSR \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x59   => sub { return ( sprintf "%02X %02X  EOR \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0x5d   => sub { return ( sprintf "%02X %02X  EOR \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x5e   => sub { return ( sprintf "%02X %02X  LSR \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x60   => sub { return ( sprintf "       RTS                             " ) },
        0x61   => sub { return ( sprintf "%02X     ADC (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0x65   => sub { return ( sprintf "%02X     ADC \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x66   => sub { return ( sprintf "%02X     ROR \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x68   => sub { return ( sprintf "       PLA                             " ) },
        0x69   => sub { return ( sprintf "%02X     ADC #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0x6a   => sub { return ( sprintf "       ROR A                           " ) },
        0x6c   => sub { return ( sprintf "%02X %02X  JMP (\$%02X%02X) = %02X%02X              ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + 1) & 0xffff), $self->read((($self->peek(1) << 8) | $self->peek(0)) & 0xffff) ) },
        0x6d   => sub { return ( sprintf "%02X %02X  ADC \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x6e   => sub { return ( sprintf "%02X %02X  ROR \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x70   => sub { return ( sprintf "%02X     BVS \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0x71   => sub { return ( sprintf "%02X     ADC (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0x75   => sub { return ( sprintf "%02X     ADC \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x76   => sub { return ( sprintf "%02X     ROR \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x78   => sub { return ( sprintf "       SEI                             " ) },
        0x79   => sub { return ( sprintf "%02X %02X  ADC \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0x7d   => sub { return ( sprintf "%02X %02X  ADC \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x7e   => sub { return ( sprintf "%02X %02X  ROR \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0x81   => sub { return ( sprintf "%02X     STA (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0x84   => sub { return ( sprintf "%02X     STY \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x85   => sub { return ( sprintf "%02X     STA \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x86   => sub { return ( sprintf "%02X     STX \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0x88   => sub { return ( sprintf "       DEY                             " ) },
        0x8a   => sub { return ( sprintf "       TXA                             " ) },
        0x8c   => sub { return ( sprintf "%02X %02X  STY \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x8d   => sub { return ( sprintf "%02X %02X  STA \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x8e   => sub { return ( sprintf "%02X %02X  STX \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0x90   => sub { return ( sprintf "%02X     BCC \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0x91   => sub { return ( sprintf "%02X     STA (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0x94   => sub { return ( sprintf "%02X     STY \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x95   => sub { return ( sprintf "%02X     STA \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0x96   => sub { return ( sprintf "%02X     STX \$%02X,Y @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{y}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{y}) & 0x00ff) ) },
        0x98   => sub { return ( sprintf "       TYA                             " ) },
        0x99   => sub { return ( sprintf "%02X %02X  STA \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0x9a   => sub { return ( sprintf "       TXS                             " ) },
        0x9d   => sub { return ( sprintf "%02X %02X  STA \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xa0   => sub { return ( sprintf "%02X     LDY #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xa1   => sub { return ( sprintf "%02X     LDA (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0xa2   => sub { return ( sprintf "%02X     LDX #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xa4   => sub { return ( sprintf "%02X     LDY \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xa5   => sub { return ( sprintf "%02X     LDA \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xa6   => sub { return ( sprintf "%02X     LDX \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xa8   => sub { return ( sprintf "       TAY                             " ) },
        0xa9   => sub { return ( sprintf "%02X     LDA #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xaa   => sub { return ( sprintf "       TAX                             " ) },
        0xac   => sub { return ( sprintf "%02X %02X  LDY \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xad   => sub { return ( sprintf "%02X %02X  LDA \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xae   => sub { return ( sprintf "%02X %02X  LDX \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xb0   => sub { return ( sprintf "%02X     BCS \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0xb1   => sub { return ( sprintf "%02X     LDA (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0xb4   => sub { return ( sprintf "%02X     LDY \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xb5   => sub { return ( sprintf "%02X     LDA \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xb6   => sub { return ( sprintf "%02X     LDX \$%02X,Y @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{y}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{y}) & 0x00ff) ) },
        0xb8   => sub { return ( sprintf "       CLV                             " ) },
        0xb9   => sub { return ( sprintf "%02X %02X  LDA \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0xba   => sub { return ( sprintf "       TSX                             " ) },
        0xbc   => sub { return ( sprintf "%02X %02X  LDY \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xbd   => sub { return ( sprintf "%02X %02X  LDA \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xbe   => sub { return ( sprintf "%02X %02X  LDX \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0xc0   => sub { return ( sprintf "%02X     CPY #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xc1   => sub { return ( sprintf "%02X     CMP (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0xc4   => sub { return ( sprintf "%02X     CPY \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xc5   => sub { return ( sprintf "%02X     CMP \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xc6   => sub { return ( sprintf "%02X     DEC \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xc8   => sub { return ( sprintf "       INY                             " ) },
        0xc9   => sub { return ( sprintf "%02X     CMP #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xca   => sub { return ( sprintf "       DEX                             " ) },
        0xcc   => sub { return ( sprintf "%02X %02X  CPY \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xcd   => sub { return ( sprintf "%02X %02X  CMP \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xce   => sub { return ( sprintf "%02X %02X  DEC \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xd0   => sub { return ( sprintf "%02X     BNE \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0xd1   => sub { return ( sprintf "%02X     CMP (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0xd5   => sub { return ( sprintf "%02X     CMP \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xd6   => sub { return ( sprintf "%02X     DEC \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xd8   => sub { return ( sprintf "       CLD                             " ) },
        0xd9   => sub { return ( sprintf "%02X %02X  CMP \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0xdd   => sub { return ( sprintf "%02X %02X  CMP \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xde   => sub { return ( sprintf "%02X %02X  DEC \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xe0   => sub { return ( sprintf "%02X     CPX #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xe1   => sub { return ( sprintf "%02X     SBC (\$%02X,X) @ %02X = %02X%02X = %02X    ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff), $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff), $self->read( (($self->read(($self->peek(0) + $self->{reg}{x} + 1) & 0x00ff) << 8) | $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff)) & 0xffff ) ) },
        0xe4   => sub { return ( sprintf "%02X     CPX \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xe5   => sub { return ( sprintf "%02X     SBC \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xe6   => sub { return ( sprintf "%02X     INC \$%02X = %02X                    ", $self->peek(0), $self->peek(0), $self->read($self->peek(0)) ) },
        0xe8   => sub { return ( sprintf "       INX                             " ) },
        0xe9   => sub { return ( sprintf "%02X     SBC #\$%02X                        ", $self->peek(0), $self->peek(0) ) },
        0xea   => sub { return ( sprintf "       NOP                             " ) },
        0xec   => sub { return ( sprintf "%02X %02X  CPX \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xed   => sub { return ( sprintf "%02X %02X  SBC \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xee   => sub { return ( sprintf "%02X %02X  INC \$%02X%02X = %02X                  ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), $self->read( (($self->peek(1) << 8) | $self->peek(0)) & 0xffff ) ) },
        0xf0   => sub { return ( sprintf "%02X     BEQ \$%04X                       ", $self->peek(0), ( $self->pc + $self->peek(0) + 1 ) & 0xffff ) },
        0xf1   => sub { return ( sprintf "%02X     SBC (\$%02X),Y = %02X%02X @ %04X = %02X  ", $self->peek(0), $self->peek(0), $self->read(($self->peek(0) + 1) & 0x00ff), $self->read($self->peek(0)), ((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff, $self->read(((($self->read(($self->peek(0) + 1) & 0x00ff) << 8) | $self->read($self->peek(0))) + $self->{reg}{y}) & 0xffff) ) },
        0xf5   => sub { return ( sprintf "%02X     SBC \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xf6   => sub { return ( sprintf "%02X     INC \$%02X,X @ %02X = %02X             ", $self->peek(0), $self->peek(0), ($self->peek(0) + $self->{reg}{x}) & 0x00ff, $self->read(($self->peek(0) + $self->{reg}{x}) & 0x00ff) ) },
        0xf8   => sub { return ( sprintf "       SED                             " ) },
        0xf9   => sub { return ( sprintf "%02X %02X  SBC \$%02X%02X,Y @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{y}) & 0xffff) ) },
        0xfd   => sub { return ( sprintf "%02X %02X  SBC \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
        0xfe   => sub { return ( sprintf "%02X %02X  INC \$%02X%02X,X @ %04X = %02X         ", $self->peek(0), $self->peek(1), $self->peek(1), $self->peek(0), (((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff), $self->read(((($self->peek(1) << 8) | $self->peek(0)) + $self->{reg}{x}) & 0xffff) ) },
    };
}

sub pc {
    my $self = shift;

    return ( (($self->{pc}{h} << 8) | $self->{pc}{l}) & 0xffff );
}

sub pc_increment {
    my $self = shift;

    my $temp = $self->pc;
    $self->{pc}{h} = ( (($temp + 1) >> 8) & 0x00ff );
    $self->{pc}{l} = ( ($temp + 1) & 0x00ff );

    return $temp;
}

sub stack_push {
    my $self = shift;
    my ( $value ) = @_;

    $self->write( ($self->{sp} | 0x0100) & 0xffff, $value );
    $self->{sp} = ( ($self->{sp} - 1) & 0x00ff );
}

sub stack_pop {
    my $self = shift;

    $self->{sp} = ( ($self->{sp} + 1) & 0x00ff );
    return $self->read( ($self->{sp} | 0x0100) & 0xffff );
}

sub fetch {
    my $self = shift;

    return $self->read( $self->pc_increment );
}

sub peek {
    my $self       = shift;
    my ( $offset ) = @_;

    my $addr = ( ($self->pc + $offset) & 0xffff );

    return $self->read( $addr );
}

sub address {
    my $self     = shift;
    my ( $mode ) = @_;

    die sprintf "< %i > no es un modo de direccionamiento valido :(", $mode unless defined $self->{addr}{$mode};

    return $self->{addr}{$mode}();
}

sub debug {
    my $self       = shift;
    my ( $opcode ) = @_;

    die sprintf "< %02X > no es un opcode definido en el debugger :(", $opcode unless defined $self->{debug}{$opcode};

    return $self->{debug}{$opcode}();
}

sub cycles {
    my $self       = shift;
    my ( $opcode ) = @_;

    die sprintf "< %02X > no es un opcode con ciclos asociados :(", $opcode unless defined $self->{cyc}{$opcode};

    return $self->{cyc}{$opcode};
}

sub read {
    my $self        = shift;
    my ( $address ) = @_;

    return $self->{read}( $address );
}

sub write {
    my $self                = shift;
    my ( $address, $value ) = @_;

    $self->{write}( $address, $value );
}

sub execute {
    my $self       = shift;
    my ( $opcode ) = @_;

    $self->{cycles} = 0 unless( DEBUG );

    # printf "%04X  %02X %s%s$/", ( ($self->pc - 1) & 0xffff ), $opcode, $self->debug( $opcode ), $self->debug( 'status' ) if( DEBUG );
    # printf "%04X  %02X %s%s$/", ( ($self->pc - 1) & 0xffff ), $opcode, $self->debug( $opcode ), $self->debug( 'status' );

    defined $self->{opc}{$opcode} ? $self->{opc}{$opcode}() : die sprintf "< %02X > no es un opcode valido :(", $opcode;
    
    unless( DEBUG ) {
        $self->{cycles} += $self->cycles( $opcode );
        return $self->{cycles};
    } else {
        $self->{cycles} += $self->cycles( $opcode ) * TIME_MULTIPLIER;
        $self->{cycles} -= 341 if( $self->{cycles} >= 341 );
        return 0;
    }
}

sub handle_nmi {
    my $self = shift;

    $self->stack_push( $self->{pc}{h} );
    $self->stack_push( $self->{pc}{l} );
    $self->stack_push( ($self->join_flags | 0x0020) & 0x00ff );
    $self->set_flags(['i']);
    $self->{pc}{h} = $self->read( 0xfffb );
    $self->{pc}{l} = $self->read( 0xfffa );

    # printf "$/NMI: %04X$/$/", $self->pc;
}

sub opcode_add_with_carry {
    my $self = shift;
    my %args = @_;

    my $value  = $self->read( $self->address($args{mode}) );
    my $result = unpack( 'S', (pack 'S', ($self->{reg}{a} + $value + $self->{flag}{c}) & 0xffff) );

    $result > 0x00ff ? $self->set_flags(['c']) : $self->clear_flags(['c']);

    my $cond1 = test_bit( ($self->{reg}{a} ^ $value) & 0x00ff, 7 );
    my $cond2 = test_bit( ($self->{reg}{a} ^ ($result & 0x00ff)) & 0x00ff, 7 );
    (! $cond1) && $cond2 ? $self->set_flags(['v']) : $self->clear_flags(['v']);
    
    $self->{reg}{a} = ( $result & 0x00ff );

    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
}

sub opcode_subtract_with_carry {
    my $self = shift;
    my %args = @_;

    my $value  = $self->read( $self->address($args{mode}) );
    my $result = unpack( 'S', (pack 'S', ($self->{reg}{a} - $value - (1 - $self->{flag}{c})) & 0xffff) );

    $result < 0x0100 ? $self->set_flags(['c']) : $self->clear_flags(['c']);

    my $cond1 = test_bit( ($self->{reg}{a} ^ $value) & 0x00ff, 7 );
    my $cond2 = test_bit( ($self->{reg}{a} ^ ($result & 0x00ff)) & 0x00ff, 7 );
    $cond1 && $cond2 ? $self->set_flags(['v']) : $self->clear_flags(['v']);
    
    $self->{reg}{a} = ( $result & 0x00ff );

    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
}

sub opcode_logical_and {
    my $self = shift;
    my %args = @_;

    my $value = $self->read( $self->address($args{mode}) );
    $self->{reg}{a} = ( $self->{reg}{a} & $value & 0x00ff );

    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_logical_xor {
    my $self = shift;
    my %args = @_;

    my $value = $self->read( $self->address($args{mode}) );
    $self->{reg}{a} = ( ($self->{reg}{a} ^ $value) & 0x00ff );

    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_logical_or {
    my $self = shift;
    my %args = @_;

    my $value = $self->read( $self->address($args{mode}) );
    $self->{reg}{a} = ( ($self->{reg}{a} | $value) & 0x00ff );

    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_branch {
    my $self = shift;
    my %args = @_;

    if( $args{condition} ) {
        my $addr = $self->address( $args{mode} );
        $self->{pc}{h} = ( ($addr >> 8) & 0x00ff );
        $self->{pc}{l} = ( $addr & 0x00ff );
    } else {
        $self->pc_increment;
    }
}

sub opcode_test_bit {
    my $self = shift;
    my %args = @_;

    my $op     = $self->read( $self->address($args{mode}) );
    my $result = ( $self->{reg}{a} & $op & 0x00ff );

    $result == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $op, 6 ) ? $self->set_flags(['v']) : $self->clear_flags(['v']);
    test_bit( $op, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_clear_flag {
    my $self = shift;
    my %args = @_;

    $self->clear_flags([ $args{flag} ]);
}

sub opcode_set_flag {
    my $self = shift;
    my %args = @_;

    $self->set_flags([ $args{flag} ]);
}

sub opcode_load_reg {
    my $self = shift;
    my %args = @_;

    $self->{reg}{ $args{reg} } = $self->read( $self->address($args{mode}) );
    $self->{reg}{ $args{reg} } == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{ $args{reg} }, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_store_reg {
    my $self = shift;
    my %args = @_;

    my $address = $self->address( $args{mode} );
    $self->write( $address, $self->{reg}{ $args{reg} } );
}

sub opcode_compare_reg {
    my $self = shift;
    my %args = @_;

    my $value = $self->read( $self->address($args{mode}) );
    my $result = ( ($self->{reg}{ $args{reg} } - $value) & 0xffff );

    $self->{reg}{ $args{reg} } >= $value ? $self->set_flags(['c']) : $self->clear_flags(['c']);
    $self->{reg}{ $args{reg} } == $value ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $result, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_increment_reg {
    my $self = shift;
    my %args = @_;

    $self->{reg}{ $args{reg} } = ( ($self->{reg}{ $args{reg} } + 1) & 0x00ff );
    $self->{reg}{ $args{reg} } == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{ $args{reg} }, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_decrement_reg {
    my $self = shift;
    my %args = @_;

    $self->{reg}{ $args{reg} } = ( ($self->{reg}{ $args{reg} } - 1) & 0x00ff );
    $self->{reg}{ $args{reg} } == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{ $args{reg} }, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_increment_memory {
    my $self = shift;
    my %args = @_;

    my $address = $self->address( $args{mode} );
    my $value   = ( ($self->read($address) + 1) & 0x00ff );

    $value == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $value, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);

    $self->write( $address, $value );
}

sub opcode_decrement_memory {
    my $self = shift;
    my %args = @_;

    my $address = $self->address( $args{mode} );
    my $value   = ( ($self->read($address) - 1) & 0x00ff );

    $value == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $value, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);

    $self->write( $address, $value );
}

sub opcode_jump {
    my $self = shift;
    my %args = @_;

    my $addr = $self->address( $args{mode} );
    $self->{pc}{h} = ( ($addr >> 8) & 0x00ff );
    $self->{pc}{l} = ( $addr & 0x00ff );
}

sub opcode_jump_to_subroutine {
    my $self = shift;
    my %args = @_;

    my $addr = ( ($self->pc + 1) & 0xffff );
    $self->stack_push( ($addr >> 8) & 0x00ff );
    $self->stack_push( $addr & 0x00ff );

    $addr = $self->address( $args{mode} );
    $self->{pc}{h} = ( ($addr >> 8) & 0x00ff );
    $self->{pc}{l} = ( $addr & 0x00ff );
}

sub opcode_no_operation {
    my $self = shift;
    my %args = @_;

    return;
}

sub opcode_push_accumulator {
    my $self = shift;
    my %args = @_;

    $self->stack_push( $self->{reg}{a} );
}

sub opcode_push_status {
    my $self = shift;
    my %args = @_;

    $self->stack_push( ($self->join_flags | 0x0030) & 0x00ff );
}

sub opcode_pull_accumulator {
    my $self = shift;
    my %args = @_;

    $self->{reg}{a} = $self->stack_pop;
    $self->{reg}{a} == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{a}, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_pull_status {
    my $self = shift;
    my %args = @_;

    my $value = $self->stack_pop;

    $self->set_flags(['p']);
    $self->clear_flags(['b']);

    test_bit( $value, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
    test_bit( $value, 6 ) ? $self->set_flags(['v']) : $self->clear_flags(['v']);
    test_bit( $value, 3 ) ? $self->set_flags(['d']) : $self->clear_flags(['d']);
    test_bit( $value, 2 ) ? $self->set_flags(['i']) : $self->clear_flags(['i']);
    test_bit( $value, 1 ) ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $value, 0 ) ? $self->set_flags(['c']) : $self->clear_flags(['c']);
}

sub opcode_return_from_subroutine {
    my $self = shift;
    my %args = @_;

    $self->{pc}{l} = $self->stack_pop;
    $self->{pc}{h} = $self->stack_pop;

    my $addr = ( ($self->pc + 1) & 0xffff );
    $self->{pc}{h} = ( ($addr >> 8) & 0x00ff );
    $self->{pc}{l} = ( $addr & 0x00ff );
}

sub opcode_return_from_interrupt {
    my $self = shift;
    my %args = @_;

    my $value = $self->stack_pop;
    test_bit( $value, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
    test_bit( $value, 6 ) ? $self->set_flags(['v']) : $self->clear_flags(['v']);
    test_bit( $value, 3 ) ? $self->set_flags(['d']) : $self->clear_flags(['d']);
    test_bit( $value, 2 ) ? $self->set_flags(['i']) : $self->clear_flags(['i']);
    test_bit( $value, 1 ) ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $value, 0 ) ? $self->set_flags(['c']) : $self->clear_flags(['c']);
    $self->set_flags(['p']);
    $self->clear_flags(['b']);

    $self->{pc}{l} = $self->stack_pop;
    $self->{pc}{h} = $self->stack_pop;
    # printf "$/PC: %04X$/$/", $self->pc;
}

sub opcode_transfer_reg {
    my $self = shift;
    my %args = @_;

    $self->{reg}{ $args{to} } = $self->{reg}{ $args{from} };
    $self->{reg}{ $args{to} } == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{ $args{to} }, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_transfer_stack_pointer_to_reg {
    my $self = shift;
    my %args = @_;

    $self->{reg}{ $args{to} } = $self->{sp};
    $self->{reg}{ $args{to} } == 0 ? $self->set_flags(['z']) : $self->clear_flags(['z']);
    test_bit( $self->{reg}{ $args{to} }, 7 ) ? $self->set_flags(['n']) : $self->clear_flags(['n']);
}

sub opcode_transfer_reg_to_stack_pointer {
    my $self = shift;
    my %args = @_;

    $self->{sp} = $self->{reg}{ $args{from} };
}

sub opcode_shift_right {
    my $self = shift;
    my %args = @_;

    my $address = $self->address( $args{mode} );
    my $value   = ( ($args{mode} != ACCUMULATOR ? $self->read($address) : $self->{reg}{a}) & 0x00ff );

    $self->clear_flags([ qw|n z c| ]);
    $self->set_flags(['c']) if( test_bit($value, 0) );

    $value = ( ($value >> 1) & 0x007f );
    $self->set_flags(['z']) if( $value == 0 );

    if( $args{mode} != ACCUMULATOR ) {
        $self->write( $address, $value );
    } else {
        $self->{reg}{a} = $value;
    }
}

sub opcode_shift_left {
    my $self = shift;
    my %args = @_;

    my $address = $self->address( $args{mode} );
    my $value   = ( ($args{mode} != ACCUMULATOR ? $self->read($address) : $self->{reg}{a}) & 0x00ff );

    $self->clear_flags([ qw|n z c| ]);
    $self->set_flags(['c']) if( test_bit($value, 7) );

    $value = ( ($value << 1) & 0x00fe );
    $self->set_flags(['z']) if( $value == 0 );
    $self->set_flags(['n']) if( test_bit($value, 7) );

    if( $args{mode} != ACCUMULATOR ) {
        $self->write( $address, $value );
    } else {
        $self->{reg}{a} = $value;
    }
}

sub opcode_rotate_right {
    my $self = shift;
    my %args = @_;

    my $address   = $self->address( $args{mode} );
    my $value     = ( ($args{mode} != ACCUMULATOR ? $self->read($address) : $self->{reg}{a}) & 0x00ff );
    my $old_carry = $self->{flag}{c};

    $self->clear_flags([ qw|n z c| ]);
    $self->set_flags(['c']) if( test_bit($value, 0) );

    $value = ( ($value >> 1) & 0x007f );
    if( $old_carry ) {
        $value = ( ($value | 0x0080) & 0x00ff );
        $self->set_flags(['n']);
    }
    $self->set_flags(['z']) if( $value == 0 );

    if( $args{mode} != ACCUMULATOR ) {
        $self->write( $address, $value );
    } else {
        $self->{reg}{a} = $value;
    }
}

sub opcode_rotate_left {
    my $self = shift;
    my %args = @_;

    my $address   = $self->address( $args{mode} );
    my $value     = ( ($args{mode} != ACCUMULATOR ? $self->read($address) : $self->{reg}{a}) & 0x00ff );
    my $old_carry = $self->{flag}{c};

    $self->clear_flags([ qw|n z c| ]);
    $self->set_flags(['c']) if( test_bit($value, 7) );

    $value = ( ($value << 1) & 0x00fe );
    $value = ( ($value | 0x0001) & 0x00ff ) if( $old_carry );
    $self->set_flags(['n']) if( test_bit($value, 7) );
    $self->set_flags(['z']) if( $value == 0 );

    if( $args{mode} != ACCUMULATOR ) {
        $self->write( $address, $value );
    } else {
        $self->{reg}{a} = $value;
    }
}

1;